﻿package idv.cjcat.emitter {
	use namespace engine;
	
	import flash.display.*;
	import flash.errors.IllegalOperationError;
	import flash.events.*;
	import flash.utils.*;
	
	import idv.cjcat.emitter.*;
	import idv.cjcat.emitter.ds.*;
	import idv.cjcat.emitter.behaviors.*;
	import idv.cjcat.emitter.deflectors.*;
	import idv.cjcat.emitter.fields.*;
	import idv.cjcat.emitter.geom.*;
	
	/**
	 * Particle emitter.
	 */
	public class Emitter extends Sprite implements IEmitterXML, IEmitterDestructible {
		
		public static const VERSION:String = "2.1.62";
		
		/**
		 * The order in which newly spawned particles are added to the container.
		 */
		public var addChildMode:uint = AddChildMode.RANDOM;
		/**
		 * Velocity field for global velocity overriding. If set to <code>null</code>, then the global velocity overriding is ignored.
		 */
		public var velocityField:Field;
		/**
		 * Global damping coefficient.
		 * <p>
		 * Zero means no damping at all, and one means everything is completely stuck still.
		 * </p>
		 */
		public var damping:Number = 0;
		
		/** @private */
		engine var _pdVec:Vector.<ParticleData> = new Vector.<ParticleData>();
		
		/** @private */
		engine var _sources:Vector.<ParticleSource> = new Vector.<ParticleSource>();
		/** @private */
		engine var _gravities:Vector.<Field> = new Vector.<Field>();
		/** @private */
		engine var _deflectors:Vector.<Deflector> = new Vector.<Deflector>();
		/** @private */
		engine var _container:DisplayObjectContainer;
		/** @private */
		engine var _timer:Number = 0;
		
		/**
		 * Creates an emitter.
		 * @param	container  The emitter for particle emission.
		 */
		public function Emitter(container:DisplayObjectContainer = null):void {
			this.id = null;
			
			this.container = container;
		}
		
		/**
		 * Applies an impulse to all particles.
		 * @param	field
		 */
		public function impulse(field:Field):void {
			if (field._destroyed) EmitterGlobal.throwDestroyedObjectError(field);
			
			var md2D:MotionData2D;
			var pd:ParticleData;
			if (field.massless) {
				for each (pd in _pdVec) {
					md2D = field.getMotionData2D(pd);
					pd.vx += md2D.x;
					pd.vy += md2D.y;
					
					MotionData2DPool.recycle(md2D);
				}
			} else {
				for each (pd in _pdVec) {
					md2D = field.getMotionData2D(pd);
					pd.vx += md2D.x / pd.mass;
					pd.vy += md2D.y / pd.mass;
					
					MotionData2DPool.recycle(md2D);
				}
			}
		}
		
		/**
		 * Call this method constantly to keep the simulation go on.
		 * <p>
		 * This method can be used as an event listener.
		 * </p>
		 * @param	e
		 */
		public function step(e:Event = null):void {
			_timer++;
			
			//regular emission
			for each (var s:ParticleSource in _sources) {
				if (s._destroyed) EmitterGlobal.throwDestroyedObjectError(s);
				
				s.spawn(this);
			}
			
			//process the live particles
			for each (var pd:ParticleData in _pdVec) {
				//process motion
				if (pd.velocityField == null) {
					//process gravity fields
					var g:Field, md2D:MotionData2D;
					for each (g in pd.particleClass._gravities) {
						if (g._destroyed) EmitterGlobal.throwDestroyedObjectError(g);
						
						if (g.active) {
							if (g.massless) {
								md2D = g.getMotionData2D(pd);
								pd.vx += md2D.x;
								pd.vy += md2D.y;
								
								MotionData2DPool.recycle(md2D);
							} else {
								md2D = g.getMotionData2D(pd);
								pd.vx += md2D.x / pd.mass;
								pd.vy += md2D.y / pd.mass;
								
								MotionData2DPool.recycle(md2D);
							}
						}
					}
					for each (g in _gravities) {
						if (g._destroyed) EmitterGlobal.throwDestroyedObjectError(g);
						
						if (g.active) {
							if (g.massless) {
								md2D = g.getMotionData2D(pd);
								pd.vx += md2D.x;
								pd.vy += md2D.y;
								
								MotionData2DPool.recycle(md2D);
							} else {
								md2D = g.getMotionData2D(pd);
								pd.vx += md2D.x / pd.mass;
								pd.vy += md2D.y / pd.mass;
								
								MotionData2DPool.recycle(md2D);
							}
						}
					}
					
					//process damping
					var finalDamping:Number = (1 - pd.damping) * (1 - this.damping);
					pd.vx *= finalDamping;
					pd.vy *= finalDamping;
					pd.cx += pd.vx;
					pd.cy += pd.vy;
					
					//process deflectors
					var d:Deflector, md4D:MotionData4D;
					for each (d in pd.particleClass._deflectors) {
						if (d._destroyed) EmitterGlobal.throwDestroyedObjectError(d);
						
						if (d.active) {
							md4D = d.getMotionData4D(pd);
							pd.cx = md4D.x;
							pd.cy = md4D.y;
							pd.vx = md4D.vx;
							pd.vy = md4D.vy;
							
							MotionData4DPool.recycle(md4D);
						}
					}
					for each (d in _deflectors) {
						if (d._destroyed) EmitterGlobal.throwDestroyedObjectError(d);
						
						if (d.active) {
							md4D = d.getMotionData4D(pd);
							pd.cx = md4D.x;
							pd.cy = md4D.y;
							pd.vx = md4D.vx;
							pd.vy = md4D.vy;
							
							MotionData4DPool.recycle(md4D);
						}
					}
					
				} else {
					var velocity:MotionData2D = pd.velocityField.getMotionData2D(pd);
					pd.vx = velocity.x;
					pd.vy = velocity.y;
					pd.cx += pd.vx;
					pd.cy += pd.vy;
					
					MotionData2DPool.recycle(velocity);
				}
				
				//process bubble motion
				if (pd.bubbleMotion) {
					//bubble motion
					var bubbleVec:Vector2D = Vector2DPool.get(pd.vx, pd.vy);
					bubbleVec.rotateThis( -90, false);
					
					var bubbleLength:Number = pd.bubbleAmplitude * Math.sin(pd.bubbleFrequency * _timer + pd.bubblePhase);
					
					bubbleVec.length = bubbleLength;
					pd.particle.x = pd.cx + bubbleVec.x;
					pd.particle.y = pd.cy + bubbleVec.y;
					
					//bubble orientation
					if (pd.bubbleOriented) {
						bubbleVec.length = pd.bubbleAmplitude * pd.bubbleFrequency * Math.cos(pd.bubbleFrequency * _timer + pd.bubblePhase);
						if (bubbleLength < 0) bubbleVec.length *= -1;
						pd.particle.rotation = Math.atan2(pd.vy + bubbleVec.y, pd.vx + bubbleVec.x) * 180 / Math.PI + pd.orientationOffset + 90;
					}
					
					Vector2DPool.recycle(bubbleVec);
				} else {
					//regular motion
					pd.particle.x = pd.cx;
					pd.particle.y = pd.cy;
				}
				
				//process orientation
				if (!pd.bubbleMotion || !pd.bubbleOriented) {
					if (pd.oriented) {
						pd.particle.rotation = Math.atan2(pd.vy, pd.vx) * 180 / Math.PI + pd.orientationOffset + 90;
					} else {
						pd.particle.rotation += pd.spin;
					}
				}
				
				//process behavior triggers;
				var bt:BehaviorTrigger;
				for each (bt in pd.particleClass._behaviorTriggers) {
					if (bt._destroyed) EmitterGlobal.throwDestroyedObjectError(bt);
					
					if (bt.active) {
						if (bt.test(pd)) {
							bt.behave(pd);
						}
					}
				}
				
				//process grow
				var dLife:Number = pd.totalLife - pd.life;
				if (dLife < pd.scaleGrowRange) {
					pd.particle.scaleX = pd.particle.scaleY = EmitterMath.interpolate(0, pd.initScale, pd.scaleGrowRange, pd.scale, dLife);
				} else {
					pd.particle.scaleX = pd.particle.scaleY = pd.scale;
				}
				if (dLife< pd.alphaGrowRange) {
					pd.particle.alpha = EmitterMath.interpolate(0, pd.initAlpha, pd.alphaGrowRange, pd.alpha, dLife);
				} else {
					pd.particle.alpha = pd.alpha;
				}
				
				//process decay
				if (pd.life < pd.scaleDecayRange) {
					pd.particle.scaleX = pd.particle.scaleY = EmitterMath.interpolate(pd.scaleDecayRange, pd.scale, 0, pd.finalScale, pd.life);
				}
				if (pd.life < pd.alphaDecayRange) {
					pd.particle.alpha = EmitterMath.interpolate(pd.alphaDecayRange, pd.alpha, 0, pd.finalAlpha, pd.life);
				}
				
				//decrement particle life
				pd.life--;
			}
			
			//filter out dead particles
			_pdVec = _pdVec.filter(pdFilter);
			/*for (var j:int = _pdVec.length - 1; j >= 0; j--) {
				if (_pdVec[j].life <= 0) {
					DisplayObjectContainer(_pdVec[j].particle.parent).removeChild(_pdVec[j].particle);
					_pdVec.splice(j, 1);
				}
			}*/
		}
		
		private function pdFilter(pd:ParticleData, index:int, vector:Vector.<ParticleData>):Boolean {
			if (pd.life <= 0) {
				DisplayObjectContainer(pd.particle.parent).removeChild(pd.particle);
				
				if (pd.particleClass.reuseMovieClip) {
					MovieClip(pd.particle).stop();
					if (pd.particle is pd.particleClass._mcPool.displayClass)
					pd.particleClass._mcPool.recycle(pd.particle as MovieClip);
				}
				
				ParticleDataPool.recycle(pd);
				return false;
			} else {
				return true;
			}
		}
		
		/**
		 * Removes all particles.
		 * <p>
		 * This method can be used as an event listener.
		 * </p>
		 * @param	e
		 */
		public function clear(e:Event = null):void {
			for each (var pd:ParticleData in _pdVec) {
				DisplayObjectContainer(pd.particle.parent).removeChild(pd.particle);
			}
			_pdVec.length = 0;
		}
		
		/**
		 * Adds a particle source.
		 * @param	source
		 */
		public function addSource(source:ParticleSource):void {
			removeSource(source);
			_sources.push(source);
		}
		/**
		 * Removes a particle source.
		 * @param	source
		 */
		public function removeSource(source:ParticleSource):void {
			var index:int = _sources.indexOf(source);
			if (index >= 0) _sources.splice(index, 1);
		}
		/**
		 * Removes all particle sources.
		 * @return The original particle sources.
		 */
		public function clearSources():Vector.<ParticleSource> {
			var temp:Vector.<ParticleSource> = _sources;
			_sources = new Vector.<ParticleSource>();
			return temp;
		}
		
		/**
		 * Adds a global gravity field.
		 * @param	gravity
		 */
		public function addGravity(gravity:Field):void {
			removeGravity(gravity);
			_gravities.push(gravity);
		}
		/**
		 * Removes a global gravity field.
		 * @param	gravity
		 */
		public function removeGravity(gravity:Field):void {
			var index:int = _gravities.indexOf(gravity);
			if (index >= 0) _gravities.splice(index, 1);
		}
		/**
		 * Removes all global gravity fields.
		 * @return The original global gravity fields.
		 */
		public function clearGravities():Vector.<Field> {
			var temp:Vector.<Field> = _gravities;
			_gravities = new Vector.<Field>();
			return temp;
		}
		
		/**
		 * Adds a global deflector.
		 * @param	deflector
		 */
		public function addDeflector(deflector:Deflector):void {
			removeDeflector(deflector);
			_deflectors.push(deflector);
		}
		/**
		 * Removes a global deflector.
		 * @param	deflector
		 */
		public function removeDeflector(deflector:Deflector):void {
			var index:int = _deflectors.indexOf(deflector);
			if (index >= 0) _deflectors.splice(index, 1);
		}
		/**
		 * Removes all global deflectors.
		 * @return The original global deflectors.
		 */
		public function clearDeflectors():Vector.<Deflector> {
			var temp:Vector.<Deflector> = _deflectors;
			_deflectors = new Vector.<Deflector>();
			return temp;
		}
		
		/**
		 * The container for particle emission.
		 * <p>
		 * If set to <code>null</code>, the emitter itself serves as the container.
		 * </p>
		 */
		public function get container():DisplayObjectContainer { return _container; }
		public function set container(value:DisplayObjectContainer):void {
			if (value == null) {
				_container = this;
			} else {
				_container = value;
			}
		}
		
		public function clone():Emitter {
			var emitter:Emitter = new Emitter();
			emitter.addChildMode = addChildMode;
			emitter._sources = _sources.concat();
			emitter._gravities = _gravities.concat();
			emitter._deflectors = _deflectors.concat();
			if (_container != this) emitter._container = _container;
			
			return emitter;
		}
		
		
		//IEmitterDestructible
		//------------------------------------------------------------------------------------------------
		
		engine var _destroyed:Boolean = false;
		public function get destroyed():Boolean { return _destroyed }
		
		//------------------------------------------------------------------------------------------------
		//end of IEmitterDestructible
		
		
		//XML
		//------------------------------------------------------------------------------------------------
		
		/** @private */
		engine var _id:String;
		public function get id():String { return _id; }
		public function set id(value:String):void {
			delete EmitterGlobal._emitters[_id];
			if (value == null) {
				while (EmitterGlobal._emitters["emitter" + EmitterGlobal._emitterCounter] != undefined) {
					EmitterGlobal._emitterCounter++;
				}
				_id = "emitter" + EmitterGlobal._emitterCounter;
			} else {
				if (EmitterGlobal._emitters[value] == undefined) {
					_id = value;
				} else {
					throw new IllegalOperationError("This name is already used by another emitter.");
				}
			}
			EmitterGlobal._emitters[_id] = this;
		}
		public function toXML():XML {
			var p:Particle;
			var s:ParticleSource;
			var g:Field;
			var d:Deflector;
			var bt:BehaviorTrigger;
			var b:Behavior;
			
			var xml:XML = <emitter/>;
			xml.@id = _id;
			xml.@addChildMode = addChildMode.toString();
			xml.@damping = damping.toString();
			
			var sourcesXML:XML = <sources/>;
			for each (s in _sources) {
				var sID:XML = <id/>;
				sID.@value = s._id;
				sourcesXML.appendChild(sID);
			}
			xml.appendChild(sourcesXML);
			
			var gravitiesXML:XML = <gravities/>;
			for each (g in _gravities) {
				var gID:XML = <id/>;
				gID.@value = g._id;
				gravitiesXML.appendChild(gID);
			}
			xml.appendChild(gravitiesXML);
			
			var deflectorsXML:XML = <deflectors/>;
			for each (d in _deflectors) {
				var dID:XML = <id/>;
				dID.@value = d._id;
				deflectorsXML.appendChild(dID);
			}
			xml.appendChild(deflectorsXML);
			
			return xml;
		}
		public function parseXML(xml:XML):void {
			addChildMode = parseInt(xml.@addChildMode);
			damping = parseFloat(xml.@damping);
			
			for each (var node:XML in xml.sources.id) {
				addSource(EmitterGlobal.getSourceByID(node.@value));
			}
		}
		
		//------------------------------------------------------------------------------------------------
		//end of XML
	}
}